NexusPi Git Node
Commit 98c9300b8be0892d7cb2b961c72bd2e12998f2f0
Parents : b1d5b33
Author : Ivan <e318cbc04468bd574db2b4523dddd710>
Signature : T66BB85Valid, signed by author
Date : 2026-08-14T12:21:41-05:00
refactor: remove MAX_NOMAD_PAGE_BYTES constant and update related tests for page download failure handling
Changes
6 files changed, 70 insertions(+), 83 deletions(-)
Diff
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f4f19941..b578e8ae 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -60,7 +60,7 @@ All notable changes to this project will be documented in this file.
- **Peer HTML sanitizers**: One XSS payload list (`javascript:`, `data:`, `<base>`, SVG, `onerror`, CSS `url()`, nested markdown) runs against MarkdownRenderer, Nomad HTML, MicronParser, and the KML sanitizer. Formatted pages still render. Scriptable nodes fail the oracle.
- **v-html**: ESLint errors on `v-html` unless the nearby disable comment names a sanitizer (`renderMarkdown`, `renderMessageHtml`, `sanitizeNomadHtml*`, `convertMicronToHtml`, and the other existing sites).
- **LAN bind**: Browser UI shows a persistent banner when bound off loopback with authentication off. Electron and Android hide it. The process does not exit.
-- **Mesh payload caps**: Announce `app_data` above 2048 bytes is omitted from storage (destination and aspect still upsert). Nomad page bodies above 512 KiB fail with `page_too_large` and a toast. Map zip `file_too_large` and RRC 350-byte line caps keep matching oracles.
+- **Mesh payload caps**: Announce `app_data` above 2048 bytes is omitted from storage (destination and aspect still upsert). Map zip `file_too_large` and RRC 350-byte line caps keep matching oracles.
- **Outbound HTTP**: New backend `httpx`/`urllib`/`aiohttp` clients must go through `ensure_outbound_http_allowed` or `http_url_guard`. Existing translator, tiles, firmware, community directory, and repository fetches are unchanged.
- **Translator (Landlock)**: On Linux, allow read/execute for user-local pipx CLIs (`~/.local/bin`, `~/.local/share/pipx`) and read-write for Argos Translate data under `~/.local/share/argos-translate`, so argospm language lists and local Argos translation work with the filesystem sandbox enabled.
- **Tests**: Landlock integration probes for subprocess spawn, translator Argos language listing, user-local CLI execution, and home write denial outside RW roots (tests/backend/test_landlock_integration_surfaces.py).
diff --git a/meshchatx.rsm b/meshchatx.rsm
index 5ab4e8c0..c4f1a2f0 100644
Binary files a/meshchatx.rsm and b/meshchatx.rsm differ
diff --git a/tests/backend/test_nomadnet_downloader.py b/tests/backend/test_nomadnet_downloader.py
index 3a5d379f..a9ca6e89 100644
--- a/tests/backend/test_nomadnet_downloader.py
+++ b/tests/backend/test_nomadnet_downloader.py
@@ -6,7 +6,6 @@ import pytest
import RNS
from meshchatx.src.backend.nomadnet_downloader import (
- MAX_NOMAD_PAGE_BYTES,
NomadnetDownloader,
NomadnetFileDownloader,
NomadnetPageDownloader,
@@ -140,43 +139,6 @@ def test_page_downloader_empty_response():
on_ok.assert_not_called()
-def test_page_downloader_rejects_oversized_body():
- on_ok = MagicMock()
- on_fail = MagicMock()
- pd = NomadnetPageDownloader(
- b"ab" * 8,
- "/page.mu",
- None,
- on_ok,
- on_fail,
- MagicMock(),
- )
- rr = MagicMock()
- rr.response = b"x" * (MAX_NOMAD_PAGE_BYTES + 1)
- pd.on_download_success(rr)
- on_fail.assert_called_once_with("page_too_large")
- on_ok.assert_not_called()
-
-
-def test_page_downloader_accepts_body_at_cap():
- on_ok = MagicMock()
- on_fail = MagicMock()
- pd = NomadnetPageDownloader(
- b"ab" * 8,
- "/page.mu",
- None,
- on_ok,
- on_fail,
- MagicMock(),
- )
- rr = MagicMock()
- rr.response = b"y" * MAX_NOMAD_PAGE_BYTES
- pd.on_download_success(rr)
- on_ok.assert_called_once()
- on_fail.assert_not_called()
- assert on_ok.call_args[0][0] == "y" * MAX_NOMAD_PAGE_BYTES
-
-
def test_file_downloader_list_response_short_list_no_crash():
on_ok = MagicMock()
on_fail = MagicMock()
diff --git a/tests/frontend/NomadNetworkPage.test.js b/tests/frontend/NomadNetworkPage.test.js
index a659bc94..0638e2df 100644
--- a/tests/frontend/NomadNetworkPage.test.js
+++ b/tests/frontend/NomadNetworkPage.test.js
@@ -991,7 +991,7 @@ describe("NomadNetworkPage.vue", () => {
wrapper.unmount();
});
- it("oversized page failure toasts failed_to_load_page instead of hanging", async () => {
+ it("page download failure toasts failed_to_load_page instead of hanging", async () => {
const wrapper = mountNomadNetworkPage({
destinationHash: "",
embedded: true,
@@ -1010,7 +1010,7 @@ describe("NomadNetworkPage.vue", () => {
status: "failure",
destination_hash: "",
page_path: "",
- failure_reason: "page_too_large",
+ failure_reason: "request_failed",
},
}),
});
diff --git a/tests/frontend/UIThemeAndVisibility.test.js b/tests/frontend/UIThemeAndVisibility.test.js
index cdcad0fd..81075617 100644
--- a/tests/frontend/UIThemeAndVisibility.test.js
+++ b/tests/frontend/UIThemeAndVisibility.test.js
@@ -89,11 +89,55 @@ const createRouterLinkStub = () => ({
props: ["to", "custom"],
});
+const mountedWrappers = [];
+
+function mountTracked(component, options) {
+ const wrapper = mount(component, options);
+ mountedWrappers.push(wrapper);
+ return wrapper;
+}
+
+function createDefaultApiMock() {
+ return {
+ get: vi.fn().mockResolvedValue({
+ data: {
+ config: {
+ theme: "light",
+ display_name: "Test User",
+ },
+ app_info: { is_reticulum_running: true },
+ },
+ }),
+ post: vi.fn().mockResolvedValue({ data: {} }),
+ patch: vi.fn().mockResolvedValue({ data: {} }),
+ };
+}
+
+beforeEach(() => {
+ document.documentElement.classList.remove("dark");
+ window.api = createDefaultApiMock();
+});
+
+afterEach(async () => {
+ await flushPromises();
+ for (const wrapper of mountedWrappers) {
+ try {
+ wrapper.unmount();
+ } catch {
+ // ignore double unmount
+ }
+ }
+ mountedWrappers.length = 0;
+ await flushPromises();
+ document.documentElement.classList.remove("dark");
+ delete window.api;
+ vi.clearAllMocks();
+});
+
describe("Theme Switching", () => {
let axiosMock;
beforeEach(() => {
- document.documentElement.classList.remove("dark");
axiosMock = {
get: vi.fn().mockResolvedValue({
data: {
@@ -118,17 +162,11 @@ describe("Theme Switching", () => {
window.api = axiosMock;
});
- afterEach(() => {
- document.documentElement.classList.remove("dark");
- delete window.api;
- vi.clearAllMocks();
- });
-
it("applies dark class to root element when theme is dark", async () => {
document.documentElement.classList.remove("dark");
expect(document.documentElement.classList.contains("dark")).toBe(false);
- const wrapper = mount(App, {
+ const wrapper = mountTracked(App, {
global: {
stubs: {
RouterView: { template: "<div>Router View</div>" },
@@ -157,7 +195,7 @@ describe("Theme Switching", () => {
it("removes dark class when theme is light", async () => {
document.documentElement.classList.add("dark");
- const wrapper = mount(App, {
+ const wrapper = mountTracked(App, {
global: {
stubs: {
RouterView: { template: "<div>Router View</div>" },
@@ -184,7 +222,7 @@ describe("Theme Switching", () => {
});
it("toggles theme from light to dark", async () => {
- const wrapper = mount(App, {
+ const wrapper = mountTracked(App, {
global: {
stubs: {
RouterView: { template: "<div>Router View</div>" },
@@ -214,7 +252,7 @@ describe("Theme Switching", () => {
});
it("toggles theme from dark to light", async () => {
- const wrapper = mount(App, {
+ const wrapper = mountTracked(App, {
global: {
stubs: {
RouterView: { template: "<div>Router View</div>" },
@@ -248,7 +286,7 @@ describe("Theme Switching", () => {
});
it("shows correct icon for theme toggle button", async () => {
- const wrapper = mount(App, {
+ const wrapper = mountTracked(App, {
global: {
stubs: {
RouterView: { template: "<div>Router View</div>" },
@@ -281,7 +319,7 @@ describe("Theme Switching", () => {
describe("Visibility Checks", () => {
it("ConfirmDialog shows when pendingConfirm is set", async () => {
- const wrapper = mount(ConfirmDialog, {
+ const wrapper = mountTracked(ConfirmDialog, {
global: {
stubs: {
MaterialDesignIcon: { template: "<div></div>" },
@@ -302,7 +340,7 @@ describe("Visibility Checks", () => {
});
it("ConfirmDialog hides when pendingConfirm is null", async () => {
- const wrapper = mount(ConfirmDialog, {
+ const wrapper = mountTracked(ConfirmDialog, {
global: {
stubs: {
MaterialDesignIcon: { template: "<div></div>" },
@@ -321,7 +359,7 @@ describe("Visibility Checks", () => {
});
it("ChangelogModal component renders correctly", () => {
- const wrapper = mount(ChangelogModal, {
+ const wrapper = mountTracked(ChangelogModal, {
global: {
stubs: {
MaterialDesignIcon: { template: "<div></div>" },
@@ -336,7 +374,7 @@ describe("Visibility Checks", () => {
});
it("Toggle shows label when provided", () => {
- const wrapper = mount(Toggle, {
+ const wrapper = mountTracked(Toggle, {
props: {
id: "test-toggle",
label: "Show Label",
@@ -348,7 +386,7 @@ describe("Visibility Checks", () => {
});
it("Toggle hides label when not provided", () => {
- const wrapper = mount(Toggle, {
+ const wrapper = mountTracked(Toggle, {
props: {
id: "test-toggle",
modelValue: false,
@@ -374,7 +412,7 @@ describe("Visibility Checks", () => {
};
window.api = axiosMock;
- const wrapper = mount(SettingsPage, {
+ const wrapper = mountTracked(SettingsPage, {
global: {
stubs: {
MaterialDesignIcon: { template: "<div></div>" },
@@ -399,8 +437,6 @@ describe("Visibility Checks", () => {
expect(wrapper.text()).toContain("app.banished_text_label");
expect(wrapper.text()).toContain("app.banished_color_label");
expect(wrapper.findAll('input[type="color"]').length).toBeGreaterThanOrEqual(1);
-
- delete window.api;
});
it("SettingsPage shows blackhole integration toggle", async () => {
@@ -416,7 +452,7 @@ describe("Visibility Checks", () => {
};
window.api = axiosMock;
- const wrapper = mount(SettingsPage, {
+ const wrapper = mountTracked(SettingsPage, {
global: {
stubs: {
MaterialDesignIcon: { template: "<div></div>" },
@@ -435,8 +471,6 @@ describe("Visibility Checks", () => {
await wrapper.vm.$nextTick();
expect(wrapper.text()).toContain("app.blackhole_integration_enabled");
-
- delete window.api;
});
it("SettingsPage hides banished config when toggle is disabled", async () => {
@@ -452,7 +486,7 @@ describe("Visibility Checks", () => {
};
window.api = axiosMock;
- const wrapper = mount(SettingsPage, {
+ const wrapper = mountTracked(SettingsPage, {
global: {
stubs: {
MaterialDesignIcon: { template: "<div></div>" },
@@ -475,14 +509,12 @@ describe("Visibility Checks", () => {
const colorInputs = wrapper.findAll('input[type="color"]');
expect(colorInputs.length).toBe(3);
-
- delete window.api;
});
});
describe("Conditional Rendering", () => {
it("App shows emergency banner when emergency mode is active", async () => {
- const wrapper = mount(App, {
+ const wrapper = mountTracked(App, {
global: {
stubs: {
RouterView: { template: "<div>Router View</div>" },
@@ -509,7 +541,7 @@ describe("Conditional Rendering", () => {
});
it("App hides emergency banner when emergency mode is inactive", async () => {
- const wrapper = mount(App, {
+ const wrapper = mountTracked(App, {
global: {
stubs: {
RouterView: { template: "<div>Router View</div>" },
@@ -536,7 +568,7 @@ describe("Conditional Rendering", () => {
});
it("App shows sidebar toggle on mobile", async () => {
- const wrapper = mount(App, {
+ const wrapper = mountTracked(App, {
global: {
stubs: {
RouterView: { template: "<div>Router View</div>" },
@@ -561,7 +593,7 @@ describe("Conditional Rendering", () => {
});
it("App shows propagation sync refresh icon on mobile", async () => {
- const wrapper = mount(App, {
+ const wrapper = mountTracked(App, {
global: {
stubs: {
RouterView: { template: "<div>Router View</div>" },
@@ -589,7 +621,7 @@ describe("Conditional Rendering", () => {
});
it("App header shows relay chat and telephone icons next to compose and sync", async () => {
- const wrapper = mount(App, {
+ const wrapper = mountTracked(App, {
global: {
stubs: {
RouterView: { template: "<div>Router View</div>" },
@@ -629,7 +661,7 @@ describe("Conditional Rendering", () => {
it("App header omits relay chat when RRC is disabled", async () => {
GlobalState.config.rrc_enabled = false;
try {
- const wrapper = mount(App, {
+ const wrapper = mountTracked(App, {
global: {
stubs: {
RouterView: { template: "<div>Router View</div>" },
@@ -659,7 +691,7 @@ describe("Conditional Rendering", () => {
describe("Dark Mode Class Application", () => {
it("App component applies dark class based on theme", async () => {
- const wrapper = mount(App, {
+ const wrapper = mountTracked(App, {
global: {
stubs: {
RouterView: { template: "<div>Router View</div>" },
@@ -698,7 +730,7 @@ describe("Dark Mode Class Application", () => {
};
window.api = axiosMock;
- const wrapper = mount(SettingsPage, {
+ const wrapper = mountTracked(SettingsPage, {
global: {
stubs: {
MaterialDesignIcon: { template: "<div></div>" },
@@ -718,8 +750,6 @@ describe("Dark Mode Class Application", () => {
const hasDarkClasses = wrapper.html().includes("dark:") || wrapper.html().includes("dark:");
expect(hasDarkClasses).toBe(true);
-
- delete window.api;
});
});
@@ -737,7 +767,7 @@ describe("Theme Persistence", () => {
};
window.api = axiosMock;
- const wrapper = mount(SettingsPage, {
+ const wrapper = mountTracked(SettingsPage, {
global: {
stubs: {
MaterialDesignIcon: { template: "<div></div>" },
@@ -761,7 +791,5 @@ describe("Theme Persistence", () => {
await wrapper.vm.$nextTick();
expect(wrapper.vm.config.theme).toBe("dark");
}
-
- delete window.api;
});
});
diff --git a/tests/frontend/behaviorContracts.test.js b/tests/frontend/behaviorContracts.test.js
index be4b87c8..7980e6d6 100644
--- a/tests/frontend/behaviorContracts.test.js
+++ b/tests/frontend/behaviorContracts.test.js
@@ -608,9 +608,6 @@ describe("behavior contracts: security gates", () => {
it("mesh payload caps stay named constants with drop-not-hang reasons", () => {
const announce = readSource("meshchatx/src/backend/announce_manager.py");
expect(announce).toContain("MAX_ANNOUNCE_APP_DATA_BYTES = 2048");
- const nomad = readSource("meshchatx/src/backend/nomadnet_downloader.py");
- expect(nomad).toContain("MAX_NOMAD_PAGE_BYTES = 512 * 1024");
- expect(nomad).toContain("page_too_large");
const rrc = readSource("meshchatx/src/backend/rrc/protocol.py");
expect(rrc).toContain("DEFAULT_MAX_MSG_BYTES = 350");
const geo = readSource("meshchatx/src/backend/map_geo_validator.py");
Served by rngit 1.5.4 - Generated in 0.05s